package com.sfc.sdsr.admin;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.util.List;

import org.apache.log4j.Logger;

import com.sfc.sdsr.common.GZipUtil;
import com.sfc.sdsr.common.Operation;
import com.sfc.sdsr.common.model.Address;
import com.sfc.sdsr.common.model.Event;
import com.sfc.sdsr.common.model.News;
import com.sfc.sdsr.common.model.Person;
import com.sfc.sdsr.common.model.Song;

public class ServerAccess {

	private final Logger log = Logger.getLogger(getClass());
	private String serverName;
	private int port;
	
	public ServerAccess(String serverName, int port) {
		this.serverName =  serverName;
		this.port = port;
	}
	
	public boolean createAddress(Address address) throws IOException {
	    byte[] data = createDataStream(Operation.CREATE_ADDRESS, address);
	    return (Boolean) sendToServer(data);
	}
	
	public boolean createMember(Person person) throws IOException {
        byte[] data = createDataStream(Operation.CREATE_MEMBER, person);
        return (Boolean)sendToServer(data);
    }
    
    public boolean createEvent(Event event) throws IOException {
        byte[] data = createDataStream(Operation.CREATE_EVENT, event);
        return (Boolean)sendToServer(data);
    }
    
    public boolean createNews(News news) throws IOException {
        byte[] data = createDataStream(Operation.CREATE_NEWS, news);
        return (Boolean)sendToServer(data);
    }
    
    public boolean createSong(Song song) throws IOException {
        byte[] data = createDataStream(Operation.CREATE_SONG, song);
        return (Boolean)sendToServer(data);
    }
    
    @SuppressWarnings("unchecked")
    public List<Address> getAddresses() throws IOException {
        byte[] data = createDataStream(Operation.GET_ADDRESSES, null);
        return (List<Address>)sendToServer(data);
    }
    
    @SuppressWarnings("unchecked")
    public List<Person> getMembers() throws IOException {
        byte[] data = createDataStream(Operation.GET_MEMBERS, null);
        return (List<Person>)sendToServer(data);
    }
    
	@SuppressWarnings("unchecked")
    public List<Event> getEvents() throws IOException {
		byte[] data = createDataStream(Operation.GET_EVENTS, null);
        return (List<Event>)sendToServer(data);
	}

	@SuppressWarnings("unchecked")
    public List<News> getNews() throws IOException {
		byte[] data = createDataStream(Operation.GET_NEWS, null);
		return (List<News>)sendToServer(data);
	}
	
	@SuppressWarnings("unchecked")
    public List<Song> getSongs() throws IOException {
		byte[] data = createDataStream(Operation.GET_SONGS, null);
        return (List<Song>)sendToServer(data);
	}

	/**
	 * creates a gziped byte stream to pass to the server. before gziped (excluding the initial 4 bytes of size),
	 * the data will have the following format
	 * 1 byte is 8 bits is 2 hex chars is max of 127 signed is 256 unsigned
	 * [4:size of stream gziped]
	 * [1:operation]
	 * [x:java Serialized object]
	 * 
	 * @param operation see com.sfc.sdsr.common.Operation for a list of possible operations
	 * @param serilizable a serilizable object. see com.sfc.sdsr.common.model for a list of possible objects
	 * @return gziped byte array
	 * @throws IOException 
	 */
	private byte[] createDataStream(byte operation, Object serilizable) throws IOException {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		ObjectOutputStream oos = new ObjectOutputStream(baos);
		oos.writeObject(serilizable);

		byte[] compressed = GZipUtil.compress(baos.toByteArray());
		int size = compressed.length;
		ByteBuffer returnBuffer = ByteBuffer.allocate(size + 4 + 1);
		returnBuffer.putInt(size);
		returnBuffer.put(operation);
		returnBuffer.put(compressed);
		return returnBuffer.array();
	}

	/**
	 * Sends data to the server, specified by the host and port of this constructor, and returns the results
	 * @param data This should be in gziped form
	 * @return an object to be checked and type casted by the calling method
	 * @throws IOException when the server isn't reachable
	 */
	private Object sendToServer(byte[] data) throws IOException {

	    InetAddress addr = InetAddress.getByName(serverName);
	    int port = this.port;
	    SocketAddress sockaddr = new InetSocketAddress(addr, port);

	    Socket socket = new Socket();

	    // this blocks untill it gains connections, 190 seconds on mac os x 10.7.2
	    // Socket socket = new Socket(serverName,port);

	    // This method will block no more than timeoutMs.
	    // If the timeout occurs, SocketTimeoutException is thrown.
	    int timeoutMs = 2000;   // 2 seconds
	    socket.connect(sockaddr, timeoutMs);
		
		// TODO make this configurable in props file;
		socket.setSoTimeout(2000);
		
		OutputStream os = socket.getOutputStream();
		os.write(data);
		
		// get the inputStream, read object and return it
		InputStream is = socket.getInputStream();
		byte[] size = new byte[4];
		is.read(size);
		
		byte[] returnObjectBytes = new byte[ByteBuffer.wrap(size).getInt()];
		is.read(returnObjectBytes);
		socket.close();
		
		byte[] uncompressedData = GZipUtil.decompress(returnObjectBytes);
		ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(uncompressedData));
		Object object = null;
		try {
			object = ois.readObject();
			if (object == null ) {
			    log.error("Server hit an exception. Theoretically when the server hits an exception, it should return null, as every command should have a return.");
			}
		} catch (ClassNotFoundException cfe) {
			log.error("failed to identify return object from server: " + cfe.getMessage(), cfe);
		} finally {
			ois.close();
		}
        return object;
	}

}
